/**
 * 简单的 jquery form 数据绑定工具。
 *
 * 该工具将自动扫描页面上的所有 form 元素，并试图绑定到 js 对象上，绑定的格式将
 * <pre>
 * html 属性
 *      data-binder-ignore           若form元素具有该属性，且取值为 true 时，整个form下的所有控件将不被绑定
 *                                   若控件该属性取值为true时，该控件将不被绑定
 *
 * 构造函数：
 *      DataBinder ()
 *      DataBinder (formId)
 *      DataBinder (options)
 *      DataBinder (map, options)
 *
 *      参数:
 *          map：    绑定的对象，将和当前 DataBinder 实例关联， 若传入 undefined / null，将由内部构建一个空对象
 *                   否则，直接使用传入的对象。
 *          options:
 *
 * <opt name> | <default>             | <type>             | <desc>
 * -----------+-----------------------+--------------------+--------------------------------------------------------
 * selector   | select,input,textarea | string or JQObject | 需要绑定form元素的jquery选择器
 * -----------+-----------------------+--------------------+--------------------------------------------------------
 * ignore     |                       | string or array    | 需要忽略的列表。
 *            |                       |                    | 若某个(些)form元素不希望被绑定，可以添加进该列表;
 *            |                       |                    | 也可直接在html标签上直接使用 data-binder-ignore="true"属性
 * -----------+-----------------------+--------------------+--------------------------------------------------------
 * form       |                       | string             | 需要绑定的form，不可和 selector 同时存在。
 *            |                       |                    | 若指定了该选项，则只有该form下的元素将会被绑定
 *
 * 公共方法
 * <signature>          | <desc>
 * ---------------------+------------------------------------------------------------------
 *    get ()            | 获取和form控件绑定的对象
 * ---------------------+------------------------------------------------------------------
 *    set (name, value) | 设置对象的值，同时反映到控件上。
 *                      | 若在多form绑定的情况下，name参数采用 formName.controlName 的形式来传递.
 *                      | 若 value 为 undefined/null时，表示清除该值
 *
 * </pre>
 * 例：
 * html 代码
 * <pre>
 *     <form id="form1">
 *         <input type = 'text' name = 'text1' />
 *         <input type = 'password' name = 'passwd' />
 *     </form>
 *     <form id='form2'>
 *         <input type = 'text' name = 'text1' />
 *     </form>
 * </pre>
 * javascript 代码
 * <pre>
 * $(function () {
 *      var binder = new DataBinder ();     // 将绑定所有的两个表单
 *      // 操作表单，比如填满所有字段值
 *      console.log (binder.get ());        // 将返回 {"form1":{"text1":"value1","passwd":"123456"},"form2":{"text1":"value2"}}
 *
 *      var binder1 = new DataBinder ('form1'); // 将只绑定form1
 *      var binder2 = new DataBinder (null, {ignore:'form2'}); // 将只绑定form1
 *
 *      var mapper  = {'orig_prop':'i am the original property'};
 *      var binder3 = new DataBinder (mapper, {}); // 使用对象 mapper 进行绑定
 *      // 操作表单，比如填满所有字段值
 *      console.log (mapper);               // 将返回 {
 *                                          //     "orig_prop":"i am the original property",
 *                                          //     "form1":{"text1":"value1","passwd":"123456"},
 *                                          //     "form2":{"text1":"value2"}
 *                                          // }
 * });
 * </pre>
 * <br/>Created by game on 2017/8/30
 */
if (!window.DataBinder) window.DataBinder = (function ($) {
    var findName = function (element) {
        var f = $(element).closest ('form');
        if (!f) {
            return null;
        }
        return f.attr ('name') || f.attr ('id') || (function () {
                $('form').each (function (index, item) {
                    if (f[0] == item[0]) {
                        return 'form_' + index;
                    }
                })
            })();
    }, findHolder = function (binder, element) {
        if (binder && binder['settings']) {
            var forms = binder ['settings']['forms'];
            if (forms > 1) {
                var name = findName (element);
                var o = binder['holder'][name];
                if (!o) {
                    o = binder['holder'][name] = {};
                }
                return o;
            } else {
                return binder ['holder'];
            }
        }
    }, setSingleValue = function (target, prefix, name, value) {
        var f = (value == undefined || value == null), i, j, c, s;
        f ? ((name in target) && delete target [name]) : target [name] = value;
        if (prefix) {
            s = '#' + prefix + ' input[name=' + name + '], #' + prefix + ' textarea[name=' + name + '], #' + prefix + ' select[name=' + name + ']';
        } else {
            s = 'input[name=' + name + '], textarea[name=' + name + '], select[name=' + name + ']';
        }
        c = $ (s);
        if (c.length > 1) { // checkbox, radio etc.
            c.prop ('checked', false);
            if (value instanceof Array) {
                c.prop ('checked', false);
                for (j = 0; j < c.length; j++) {
                    for (i = 0; i < value.length; i++) {
                        if ($ (c [j]).val () == value[i]) {
                            c [j].checked = true;
                            break;
                        }
                    }
                }
            } else {
                for (i = 0; i < c.length; i++) {
                    if ($ (c[i]).val () == value) {
                        c[i].checked = true;
                        break;
                    }
                }
            }
        } else if (c.length == 1) {
            if ('checkbox' == c.attr ('type')) {
                c.prop ('checked', !f);
            } else if (c.is ('select')) {
                f ? c.prop ('selectedIndex', -1) : c.val (value);
            } else if (c.is ('input') || c.is ('textarea')) {
                f ? c.val ('') : c.val (value);
            }
        }
    }, remove = function (holder, target, prefix) {
        var hasProp = false;
        for (var k in target) {
            if (target.hasOwnProperty (k)) {
                hasProp = true;
                break;
            }
        }
        if (!hasProp) {
            delete holder [prefix];
        }
    }, binder = function () {
        var thiz = this, options, holder;

        if (arguments.length == 1) {
            if (typeof arguments[0] == 'string') {
                options = {form:arguments[0]};
            } else {
                options = arguments [0];
            }
        }
        if (arguments.length >= 2) {
            holder = arguments [0];
            options = arguments [1];
        }

        this.holder = holder || {};
        this.settings = $.extend ({}, options || {});
        this.settings['forms'] = (function () {
            var count = 0, forms = $('form');
            for (var i = 0; i < forms.length; i ++) {
                if ($(forms [i]).attr ('data-binder-ignore') == 'true') {
                    continue;
                }

                count ++;
            }
            return count;
        })();

        var selector = this.settings ['selector'] || (function (opts) {
                if ('form' in opts) {
                    thiz.settings ['forms'] = 1;
                    var form = opts ['form'];
                    return '#' + form + ' select,#' + form + ' input,#' + form + ' textarea';
                }
            })(this.settings) || 'form[data-binder-ignore!=true] select, form[data-binder-ignore!=true] input, form[data-binder-ignore!=true] textarea';
        $ (function () {
            $ (selector).on ('change', function () {
                var self = $ (this), key = self.attr ('name') || self.attr ('id') || undefined, value, ignore;
                if (!key)
                    return;

                ignore = (self.attr ('data-binder-ignore') == 'true') ||
                    (('ignore' in thiz.settings) && (thiz.settings ['ignore'].indexOf (key)) >= 0);
                if (ignore)
                    return;

                var map = findHolder (thiz, this);
                value = self.val ();
                if ('checkbox' == self.attr ('type')) {
                    if (self.is (':checked')) {
                        if (key in map) {
                            var o = map [key];
                            (o instanceof Array) ? o.push (value) : map [key] = [o, value];
                        } else {
                            map [key] = value;
                        }
                    } else {
                        if (key in map) {
                            o = map [key];
                            if (o instanceof Array) {
                                for (var i = 0; i < o.length; i++) {
                                    if (o [i] == value) {
                                        for (var j = i; j < o.length - 1; j++) {
                                            o [j] = o [j + 1];
                                        }
                                        o.length--;
                                        if (o.length == 1) {
                                            map [key] = o [0];
                                        }
                                        break;
                                    }
                                }
                            } else {
                                delete map [key];
                            }
                        }
                    }
                } else {
                    if (value == '') {
                        if (key in map) {
                            delete map [key];
                        }
                    } else {
                        map [key] = value;
                    }
                }

                remove (thiz.holder, map, findName (this));
            });
        });
    };

    binder.prototype = {
        get:function () {
            return this.holder;
        },
        set:function (name, value) {
            if (!this.settings.forms || this.settings.forms == 1) {
                setSingleValue (this.holder, null, name, value);
            } else if (name.indexOf ('.') > 0) {
                var array = name.split ('.'), prefix=array[0], pName=array[1],
                    target = this.holder [prefix];
                setSingleValue (target, prefix, pName, value);

                remove (this.holder, target, prefix);
            }
        }
    };

    return binder;
}) (jQuery);